package moltencore.network.game;

import moltencore.game.*;
import moltencore.network.game.exception.FullGameException;
import moltencore.network.game.exception.JoinTeamException;
import moltencore.network.serverlobby.exception.LobbyCreationException;
import moltencore.player.PlayerList;
import moltencore.player.exception.PlayerAlreadyRegisteredException;
import moltencore.player.exception.PlayerNameAlreadyExistsException;
import org.apache.log4j.Logger;
import spark.utility.IncrementalLongIdFactory;

import java.util.*;

public class GameLobby {
	private static Logger logger = Logger.getLogger(GameLobby.class);

	private static final int SPECTATORS_INDEX = -1;

	private IncrementalLongIdFactory clientIdFactory = new IncrementalLongIdFactory();
	private Settings settings;
	private Collection<Field> fields;
	private Player[][] teams;
	private ArrayList<Player> spectators;
	private PlayerList<Player> players;
	private LinkedList<IGameEventListener> listeners = new LinkedList<IGameEventListener>();

	public GameLobby(Settings settings, Collection<Field> fields) throws LobbyCreationException {
		this.settings = settings;
		this.players = new PlayerList<Player>(settings.getPlayerLimit());
		this.fields = fields;
		spectators = new ArrayList<Player>(settings.getPlayerLimit());
		// if teams are disabled put all players into one and the same global team
		if(settings.getPlayerLimit() % settings.getTeamSize() != 0) {
			throw new LobbyCreationException("Player limit " + settings.getPlayerLimit() + " set incorrectly for team size of " + settings.getTeamSize());
		}
		teams = new Player[settings.getPlayerLimit() / settings.getTeamSize()][settings.getTeamSize()];
		int expectedFieldCount = teams.length;
		if(fields.size() != expectedFieldCount) {
			throw new LobbyCreationException("There are " + fields.size() + " fields, provided settings require " + expectedFieldCount);
		}
	}

	/**
	 * Add a new player to the game. The player will have a status of {@link Player.State#Joined}.
	 *
	 * @param name                Player's name.
	 * @param session             Player's session.
	 * @param cannonConfiguration Player's cannon properties.
	 * @param bubbleSetId         Id of the players bubble set. Only needed for visualization.
	 * @return Player that has been added to the game.
	 * @throws FullGameException if the game has already reached the limit of players it can hold.
	 * @throws moltencore.player.exception.PlayerAlreadyRegisteredException
	 *                           If a player with this {@code session} has already joined this game.
	 * @throws moltencore.player.exception.PlayerNameAlreadyExistsException
	 *                           If a player with this {@code name} has alraedy joined this game.
	 */
	public Player addPlayer(String name, byte bubbleSetId, long session, CannonConfiguration cannonConfiguration) throws FullGameException, PlayerAlreadyRegisteredException, PlayerNameAlreadyExistsException {
		if(players.size() == settings.getPlayerLimit()) {
			throw new FullGameException("The game is full");
		}
		Player player = new Player(name, bubbleSetId, session, clientIdFactory.getNextId(), cannonConfiguration);
		players.login(player);
		for(IGameEventListener gameEventListener : listeners) {
			player.addGameEventListener(gameEventListener);
		}
		//if teams are disabled assign the player to the first free one-man-team
		if(!settings.isTeamsEnabled()) {
			for(int i = 0; i < teams.length; i++) {
				Player[] team = teams[i];
				if(team[0] == null) {
					try {
						playerJoinTeam(session, i, 0);
					}
					catch(JoinTeamException e) {
						logger.error(e);
					}
					break;
				}
			}
		}
		//else assign him as a spectator
		else {
			spectators.add(player);
		}
		notifyListenersPlayerJoined(player);
		return player;
	}

	/**
	 * Assigns a player who is already logged in to a team's slot.
	 *
	 * @param session       Session of the player to assign the slot
	 * @param teamIndex     Index of a team
	 * @param teamSlotIndex Team slot index
	 * @throws moltencore.network.game.exception.JoinTeamException
	 *          If the slot is already occupied by another player
	 */
	public void playerJoinTeam(long session, int teamIndex, int teamSlotIndex) throws JoinTeamException {
		if(teamIndex > teams.length - 1) {
			throw new JoinTeamException("Invalid team index " + teamIndex);
		}
		if(teamSlotIndex > teams[teamIndex].length - 1) {
			throw new JoinTeamException("Invalid slot index " + teamSlotIndex + " on team with index " + teamIndex);
		}
		if(teams[teamIndex][teamSlotIndex] != null) {
			throw new JoinTeamException("Can't join this team's slot because it's already taken");
		}
		Player player = players.getPlayer(session);
		removePlayer(player);
		teams[teamIndex][teamSlotIndex] = player;
		player.setPositionIndex(teamSlotIndex);
		notifyListenersPlayerJoinedTeam(player, teamIndex);
	}

	public void playerJoinSpectators(long session) {
		Player player = players.getPlayer(session);
		removePlayer(player);
		spectators.add(player);
		notifyListenersPlayerJoinedTeam(player, SPECTATORS_INDEX);
	}

	/**
	 * Used internally to remove a player from his old position (spectators or team) before assigning him to a new one
	 *
	 * @param player Player in question
	 */
	private void removePlayer(Player player) {
		//clear the players previous position by removing him from the spectators (if he is in there)
		boolean removed = spectators.remove(player);
		//and if he isn't iterate over all the teams and find him
		if(!removed) {
			for(Player[] team : teams) {
				for(int i = 0; i < team.length; i++) {
					Player previousPositionPlayer = team[i];
					if(player.equals(previousPositionPlayer)) {
						team[i] = null;
					}
				}
			}
		}
	}

	/**
	 * Assigns the teams players to the actual game fields
	 */
	public void assignTeamsToFields() {
		Iterator<Field> fieldsIterator = fields.iterator();
		for(Player[] team : teams) {
			Field field = fieldsIterator.next();
			for(Player player : team) {
				if(player != null) {
					player.setField(field);
				}
			}
		}
	}

	public PlayerList<Player> getPlayers() {
		return players;
	}

	public Collection<Field> getFields() {
		return fields;
	}

	public Settings getSettings() {
		return settings;
	}

	public Collection<PositionedPlayer> getPositionedPlayers() {
		LinkedList<PositionedPlayer> positionedPlayers = new LinkedList<PositionedPlayer>();
		for(int i = 0; i < teams.length; i++) {
			Player[] team = teams[i];
			for(Player player : team) {
				if(player != null) {
					positionedPlayers.add(new PositionedPlayer(i, player));
				}
			}
		}
		for(Player spectator : spectators) {
			positionedPlayers.add(new PositionedPlayer(SPECTATORS_INDEX, spectator));
		}
		return positionedPlayers;
	}

	public void addGameEventListener(IGameEventListener listener) {
		listeners.add(listener);
		for(Player player : players.getOnlinePlayers()) {
			player.addGameEventListener(listener);
		}
	}

	protected void notifyListenersPlayerJoined(Player player) {
		for(IGameEventListener gameEventListener : listeners) {
			gameEventListener.playerJoinedGame(player);
		}
	}

	protected void notifyListenersPlayerJoinedTeam(Player player, int teamIndex) {
		for(IGameEventListener gameEventListener : listeners) {
			gameEventListener.playerJoinedTeam(player, teamIndex);
		}
	}

	public static class PositionedPlayer {
		private int teamIndex;
		private Player player;

		public PositionedPlayer(int teamIndex, Player player) {
			this.teamIndex = teamIndex;
			this.player = player;
		}

		public int getTeamIndex() {
			return teamIndex;
		}

		public void setTeamIndex(int teamIndex) {
			this.teamIndex = teamIndex;
		}

		public Player getPlayer() {
			return player;
		}

		public void setPlayer(Player player) {
			this.player = player;
		}
	}
}
